
package com.joysuch.wwyt.core.service.impl;

import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.UUID;
import com.joysuch.wwyt.core.bean.BatchUploadInfoBean;
import com.joysuch.wwyt.core.entity.BaseImageRepository;
import com.joysuch.wwyt.core.service.BaseAttachmentRepositoryService;
import com.joysuch.wwyt.core.service.BaseImageRepositoryService;
import com.joysuch.wwyt.core.service.ZipProcessService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.MalformedInputException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

@Service
@Slf4j
public class ZipProcessServiceImpl implements ZipProcessService {

    private static final Logger logger = LoggerFactory.getLogger(ZipProcessServiceImpl.class);

    @Autowired
    private BaseImageRepositoryService baseImageRepositoryService;
    @Autowired
    private BaseAttachmentRepositoryService baseAttachmentRepositoryService;
    @Value("${wwyt.upload.unzip.tempdir}")
    private String unzipTempDir;
    private String[] defaultImageSuffixes = new String[]{"jpg", "jpeg", "png", "gif"};

    @PostConstruct
    void confirmUnzipTempDir() {
        // 未设置临时目录，使用系统临时目录
        if (StringUtils.isBlank(unzipTempDir)) {
            unzipTempDir = System.getProperty("java.io.tmpdir");
//        unzipTempDir = "F:\\upload\\zip";
        }
        if (!new File(unzipTempDir).canWrite()) {
            throw new IllegalStateException("zip解压目录没有写入文件权限，请调整。文件目录为: " + unzipTempDir);
        }
    }

    @Override
    public List<BatchUploadInfoBean> batchUploadImages(InputStream zip, boolean saveInDb, int maxDirDepth,
                                                       Charset charset, String... avaliableSuffixes) throws MalformedInputException {
        File unzipDir = getUniqueUnzipDir();
        unzip(zip, unzipDir, charset == null ? Charset.defaultCharset() : charset);
        // 扫描符合条件的目录
        List[] re = scanFileInDir(unzipDir, maxDirDepth,
                (avaliableSuffixes == null || avaliableSuffixes.length == 0) ? defaultImageSuffixes
                        : avaliableSuffixes);
        List<File> files = re[0];
        List<BatchUploadInfoBean> result = files.stream().map(f -> {
            BatchUploadInfoBean bean = new BatchUploadInfoBean();
            bean.setBaseName(FilenameUtils.getBaseName(f.getName()));
            bean.setFile(f);
            bean.setFilename(f.getName());
            bean.setSuffix(FilenameUtils.getExtension(f.getName()));
            bean.setTempDir(unzipDir.getName());
            bean.setDirPath(unzipDir.getAbsolutePath());
            if (saveInDb) {
                try {
                    BaseImageRepository image = baseImageRepositoryService.getSaveImage(f);
                    bean.setId(image.getId());
                    bean.setPath(image.getOriginPath());
                } catch (IOException e) {
                    throw new RuntimeException("保存文件到数据库出错:" + f.getAbsolutePath(), e);
                }
            }
            return bean;
        }).collect(Collectors.toList());
        addIgnoredItems(re[1], result);
        return result;
    }

    private void addIgnoredItems(List<ScanItem> list, List<BatchUploadInfoBean> result) {
        list.forEach(b -> {
            File f = b.file;
            BatchUploadInfoBean bean = new BatchUploadInfoBean();
            bean.setBaseName(FilenameUtils.getBaseName(f.getName()));
            bean.setFile(f);
            bean.setFilename(f.getName());
            bean.setSuffix(FilenameUtils.getExtension(f.getName()));
            bean.setIgnored(true);
            bean.setIgnoreReason(b.msg);
            result.add(bean);
        });
    }

    @Override
    public void deleteTempDir(String tempDir) {
        try {
            FileUtils.forceDelete(new File(unzipTempDir, tempDir));
        } catch (IOException e) {
            logger.error("清空临时目录{}出错", unzipTempDir + "/" + tempDir);
        }
    }

    private List[] scanFileInDir(File unzipDir, int maxDirDepth, String[] avaliableSuffixes) {
        List<File> result = new ArrayList<>();
        List<File> needScanDir = new ArrayList<>();
        // 存放每个文件对应的目录深度
        Map<File, Integer> fileDepthMap = new HashMap<>();
        needScanDir.add(unzipDir);
        fileDepthMap.put(unzipDir, 0);

        List<ScanItem> ignoreList = new ArrayList<>();
        while (needScanDir.size() > 0) {
            File toScan = needScanDir.remove(0);
            int currentDepth = fileDepthMap.get(toScan);
            if (maxDirDepth != 0 && currentDepth > maxDirDepth) {
                // 设置了最大深度.压缩包解压后的内容算第1级.如下所示，A是压缩包中的顶级目录，maxDepth=1允许扫描A中文件，不允许扫描B中文件。
                // 例 - A
                // _____|- B
                // _____|- c.jpg
                break;
            }
            File[] listFiles = toScan.listFiles(new FileFilter() {

                @Override
                public boolean accept(File f) {
                    boolean b = f.isDirectory() || (avaliableSuffixes == null || avaliableSuffixes.length == 0)
                            || isSuffixRight(FilenameUtils.getExtension(f.getName()), avaliableSuffixes);
                    if(f.isDirectory()) {
                        if(currentDepth == maxDirDepth) {
                            // 最后一级，不再深入
                            ignoreList.add(new ScanItem(f, true, "文件夹"));
                        }
                    } else {
                        if(avaliableSuffixes != null && !isSuffixRight(FilenameUtils.getExtension(f.getName()), avaliableSuffixes)) {
                            ignoreList.add(new ScanItem(f, true, "文件类型不支持"));
                        }
                    }
                    return b;
                }
            });

            for (int i = 0; i < listFiles.length; i++) {
                if (listFiles[i].isFile()) {
                    result.add(listFiles[i]);
                } else {
                    needScanDir.add(listFiles[i]);
                    fileDepthMap.put(listFiles[i], currentDepth + 1);
                }
            }
            fileDepthMap.remove(toScan);
        }
        return new List[] {result, ignoreList};
    }

    protected boolean isSuffixRight(String extension, String[] avaliableSuffixes) {
        for (int i = 0; i < avaliableSuffixes.length; i++) {
            if (avaliableSuffixes[i].equalsIgnoreCase(extension)) {
                return true;
            }
        }
        return false;
    }

    private File getUniqueUnzipDir() {
        File dir = null;
        do {
            UUID uuid = UUID.randomUUID();
            dir = new File(unzipTempDir, uuid.toString());
        } while (dir.exists());
        return dir;
    }

    @Override
    public List<BatchUploadInfoBean> batchUploadImages(InputStream zip, boolean saveInDb, String... avaliableSuffixes) throws MalformedInputException {
        return batchUploadImages(zip, saveInDb, 1, Charset.defaultCharset(), avaliableSuffixes);
    }

    @Override
    public List<BatchUploadInfoBean> batchUploadAttachments(InputStream zip, boolean saveInDb, int maxDirDepth,
                                                            Charset charset, String... avaliableSuffixes) throws MalformedInputException {
        File unzipDir = getUniqueUnzipDir();
        unzip(zip, unzipDir, charset == null ? Charset.defaultCharset() : charset);
        // 扫描符合条件的目录
        List[] re = scanFileInDir(unzipDir, maxDirDepth,
                (avaliableSuffixes == null || avaliableSuffixes.length == 0) ? defaultImageSuffixes
                        : avaliableSuffixes);
        List<File> files = re[0];
                System.err.println(files);
        List<BatchUploadInfoBean> result = files.stream().map(f -> {
            BatchUploadInfoBean bean = new BatchUploadInfoBean();
            bean.setBaseName(FilenameUtils.getBaseName(f.getName()));
            bean.setFile(f);
            bean.setFilename(f.getName());
            bean.setSuffix(FilenameUtils.getExtension(f.getName()));
            bean.setTempDir(unzipDir.getName());
            if (saveInDb) {
                try {
                    bean.setId(baseAttachmentRepositoryService.saveFile(f));
                } catch (Exception e) {
                    throw new RuntimeException("保存文件到数据库出错:" + f.getAbsolutePath(), e);
                }
            }
            return bean;
        }).collect(Collectors.toList());
        addIgnoredItems(re[1], result);
        return result;
    }

    void unzip(InputStream in, File unzipDir, Charset charset) throws MalformedInputException {
        ZipInputStream zipInputStream = new ZipInputStream(in, charset);
        read(zipInputStream, (zipEntry) -> {
            if (zipEntry.getName().contains("__MACOSX")) {
                return;
            }
            // FileUtil.file会检查slip漏洞，漏洞说明见http://blog.nsfocus.net/zip-slip-2/
            File outItemFile = new File(unzipDir, zipEntry.getName());
            if (zipEntry.isDirectory()) {
                // 目录
                // noinspection ResultOfMethodCallIgnored
                outItemFile.mkdirs();
            } else {
                // 文件
                try {
                    FileUtils.forceMkdir(outItemFile.getParentFile());
                    IOUtils.copy(zipInputStream, new FileOutputStream(outItemFile), 2048);
                } catch (Exception e) {
                    throw new RuntimeException("解压文件异常:" + outItemFile.getName(), e);
                }
            }
        });
    }

    void read(ZipInputStream zipStream, Consumer<ZipEntry> consumer) throws MalformedInputException {
        try {
            ZipEntry zipEntry;
            File outItemFile;
            while (null != (zipEntry = zipStream.getNextEntry())) {
                consumer.accept(zipEntry);
            }
        } catch (MalformedInputException e) {
            throw e;
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            IoUtil.close(zipStream);
        }
    }

    @Override
    public List<BatchUploadInfoBean> batchUploadAttachments(InputStream zip, boolean saveInDb,
                                                            String... avaliableSuffixes) throws MalformedInputException {
        return batchUploadAttachments(zip, saveInDb, 1, Charset.defaultCharset(), avaliableSuffixes);
    }


    @Override
    public void copyFile(File file, File newFile) {
        try {
            BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(newFile));
            byte[] bys = new byte[1024];
            int len = 0;
            while ((len = bis.read(bys)) != -1) {
                bos.write(bys, 0, len);
            }
            bos.close();
            bis.close();
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }

    }

    static class ScanItem {
        File file;
        boolean ignored;
        String msg;

        public ScanItem(File file, boolean ignored, String msg) {
            this.file = file;
            this.ignored = ignored;
            this.msg = msg;
        }
    }
}
